package nayasis.common.ui.javaFx.controller;

import java.util.List;
import java.util.UUID;

import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TableView.TableViewSelectionModel;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.util.Callback;
import nayasis.common.model.NData;
import nayasis.common.model.NMultiData;

@SuppressWarnings("rawtypes")
public class NfxTable {

	private ObservableList<NData> dataBody   = FXCollections.observableArrayList();
	private NData                 dataHeader = new NData();
	private TableView             tableView  = null;
	
	private NMultiData            eventList  = new NMultiData();
	
	/**
	 * 기본 생성자
	 * 
	 * @param tableView 기능확장을 수행할 원본 객체
	 * @param columnId  row에 binding시킬 컬럼ID
	 */
	@SuppressWarnings({ "unchecked" })
	public NfxTable( TableView tableView, String... columnId ) {
	
		if( tableView == null ) return;
		
		this.tableView = tableView;
		this.tableView.setItems( dataBody );

		setColumnMapping( columnId );
		
	}

	@SuppressWarnings("unchecked")
	private void setColumnMapping( String... columnId ) {

		ObservableList<TableColumn> columns = this.tableView.getColumns();
		
		int columnIdLength = columnId.length;
		
		String id = null;
		
		for( int i = 0, iCnt = columns.size(); i < iCnt; i++ ) {

			TableColumn column = columns.get( i );

			if( i <= columnIdLength - 1 ) {
				id = columnId[ i ];
			} else {
				id = column.getId();
			}
			
			if( i > columnIdLength || id == null ) {
				id = String.format( "%s_%d", tableView.getId(), i );
			}
			
			setColumnValueModel( column, id );

		}
	}
	
	@SuppressWarnings({ "unchecked" })
	private void setColumnValueModel( TableColumn column, final String columnId ) {

		column.setId( columnId );

		column.setCellValueFactory( new Callback<CellDataFeatures<NData,String>, ObservableValue<String>>(){
			public ObservableValue<String> call( CellDataFeatures<NData, String> param ) {
				return new SimpleStringProperty( param.getValue().getString(columnId) );
			}                   
		});

		dataHeader.put( columnId );
	}
	

	/**
	 * Wrapping된 원래 TableView 객체를 가져온다.
	 * 
	 * @return 원래 TableView 객체
	 */
	public TableView getInstance() {
		return this.tableView;
	}
	
	/**
	 * row를 추가한다.
	 * 
	 * @param data 데이터
	 */
	public void addRow( NData data ) {
		
		if( data == null ) data = new NData();
		
		dataBody.add( data );

	}

	/**
	 * row들을 추가한다.
	 * 
	 * @param multiData 추가할 데이터
	 */
	public void addRows( NMultiData multiData ) {
		
		for( NData row : multiData ) {
			addRow( row );
		}
		
	}

	/**
	 * row를 삭제한다.
	 * 
	 * @param index row index
	 */
	public void removeRow( int index ) {
		dataBody.remove( index );
	}

	/**
	 * 데이터를 가져온다.
	 * 
	 * @param rowIndex row index
	 * @return row를 구성하고 있는 데이터
	 */
	public NData getData( int rowIndex ) {
		return dataBody.get( rowIndex );
	}

	/**
	 * 테이블에 바인딩된 모든 데이터를 가져온다.
	 * 
	 * @return 모든 데이터 (읽기전용)
	 */
	public NMultiData getAllData() {

		NMultiData data = new NMultiData();
		
		for( NData row : dataBody ) {
			data.add( row );
		}
		
		return data;
		
	}


	/**
	 * 테이블의 row 개수를 구한다.
	 * 
	 * @return row 개수
	 */
	public int getRowSize() {
		return dataBody.size();
	}
	
	/**
	 * 테이블의 column 개수를 구한다.
	 * 
	 * @return column 개수
	 */
	public int getColumnSize() {
		return dataHeader.size();
	}
	
	/**
	 * 데이터를 세팅한다.
	 * 
	 * @param rowIndex row index
	 * @param data 데이터
	 */
	public void setRow( int rowIndex, NData data ) {
		dataBody.set( rowIndex, data );
	}

	/**
	 * 데이터를 교체한다.
	 * 
	 * @param rows 교체할 데이터
	 */
	public void bind( NMultiData rows ) {
		clear();
		addRows( rows );
	}

	/**
	 * 선택된 row index 목록을 가져온다.
	 * 
	 * @return 선택된 row index 목록
	 */
	public List<Integer> getSelectedRowIndices() {
		
		TableViewSelectionModel<?> selectionModel = tableView.getSelectionModel();
		
		return selectionModel.getSelectedIndices();
		
	}

	/**
	 * 현재 focus된 cell 을 가져온다.
	 * 
	 * @return 현재 focus된 cell
	 */
	private TablePosition getFocusedCell() {
		
		return tableView.getFocusModel().getFocusedCell();
		
	}

	/**
	 * 현재 focus된 테이블의 row 위치를 구한다.
	 * 
	 * @return focuse된 테이블의 row 위치
	 */
	public int getFocusedRowIndex() {
		return getFocusedCell().getRow();
	}

	/**
	 * 현재 focus된 테이블의 column 위치를 구한다.
	 * 
	 * @return focuse된 테이블의 column 위치
	 */
	public int getFocusedColumnIndex() {
		return getFocusedCell().getColumn();
	}

	/**
	 * 특정 row가 중앙에 위치하도록 테이블을 스크롤한다.
	 * 
	 * <pre>
	 * 
	 * </pre>
	 * 
	 * @param rowIndex
	 */
	private void scrollTo( final int rowIndex ) {

		// there is some issue on drawing TableView.
		// So, I have to call "scrollTo" method via "Platfomr.runLater"
		// for avoiding IllegalStateException
		
		Platform.runLater( new Thread() {
			
			public void run() {
				
				try {
					
					tableView.scrollTo( rowIndex );
					
				} catch( IllegalStateException e ) {}
				
			}
		});
		
	}

	/**
	 * 지정한 위치의 row로 focus를 이동한다.
	 * 
	 * @param rowIndex row index
	 */
	public void focusRow( int rowIndex ) {
		
		tableView.getFocusModel().focus( rowIndex );
		
		scrollTo( rowIndex );
		
	}
	
	/**
	 * 지정한 위치의 column으로 focus를 이동한다.
	 * 
	 * @param columnIndex column index
	 */
	public void focusColumn( int columnIndex ) {
		
		focus( getFocusedRowIndex(), columnIndex );

	}

	/**
	 * 지정한 위치로 focus를 이동한다.
	 * 
	 * @param rowIndex    row index
	 * @param columnIndex column index
	 */
	public void focus( int rowIndex, int columnIndex ) {

		@SuppressWarnings("unchecked")
		TablePosition position = new TablePosition( tableView, rowIndex, getColumn(columnIndex) );
		
		tableView.getFocusModel().focus( position );
		
		scrollTo( rowIndex );
		
	}
	

	/**
	 * 지정한 위치의 row를 선택한다.
	 * 
	 * @param rowIndex row index
	 */
	public void select( int rowIndex ) {

	    if( rowIndex < 0 ) {
	        rowIndex = 0;
	    } else if( rowIndex > dataBody.size() - 1 ) {
	        rowIndex = dataBody.size() - 1;
	    }
	    
		tableView.getSelectionModel().select( rowIndex );
		
		scrollTo( rowIndex );
		
	}
	
	/**
	 * 한 칸 아래로 row를 선택한다.
	 */
	public void selectNext() {
		
		select( getFocusedRowIndex() + 1 );
		
	}
	
	/**
	 * 한 칸 위로 row를 선택한다.
	 */
	public void selectPrevious() {
		
		select( getFocusedRowIndex() - 1 );
		
	}
	
	private TableColumn getColumn( int index ) {
		
		ObservableList columns = tableView.getColumns();
		
		if( index < 0 ) {
			index = 0;
		} else {
			index = Math.min( index, columns.size() - 1 );
		}
		
		Object column = columns.get( index );
		
		return ( column == null ) ? null : (TableColumn) column;
		
	}
	
	
	/**
	 * 테이블 화면을 갱신한다.
	 */
	private void refresh() {
		
		if( getColumnSize() == 0 ) return;
		
		TableColumn column = (TableColumn) tableView.getColumns().get(0);
		
		column.setVisible( false );
		column.setVisible( true );
		
	}
	
	/**
	 * 데이터를 초기화시킨다.
	 */
	public void clear() {
		dataBody.clear();
		refresh();
	}
	

	/**
	 * Key press 시 수행할 이벤트를 추가한다.
	 * 
	 * @param eventHandler 추가할 이벤트
	 * @return eventId (나중에 제거할 때 기준값으로 사용됨)
	 */
	public String addEventKeypress( EventHandler<KeyEvent> eventHandler ) {
		return addEvent( KeyEvent.KEY_PRESSED, eventHandler );
	}

	/**
	 * Key release 시 수행할 이벤트를 추가한다.
	 * 
	 * @param eventHandler 추가할 이벤트
	 * @return eventId (나중에 제거할 때 기준값으로 사용됨)
	 */
	public String addEventKeyup( EventHandler<KeyEvent> eventHandler ) {
		return addEvent( KeyEvent.KEY_RELEASED, eventHandler );
	}

	/**
	 * 마우스 클릭시 수행할 이벤트를 추가한다.
	 * 
	 * @param eventHandler 추가할 이벤트
	 * @return eventId (나중에 제거할 때 기준값으로 사용됨)
	 */
	public String addEventClick( EventHandler<MouseEvent> eventHandler ) {
		return addEvent( MouseEvent.MOUSE_CLICKED, eventHandler );
	}

	/**
	 * 이벤트를 추가한다.
	 * @param <T>
	 * 
	 * @param eventType    이벤트 유형
	 * @param eventHandler 추가할 이벤트
	 * @return eventId (나중에 이벤트를 제거할 때 기준값으로 사용됨)
	 */
	public <T extends Event> String addEvent( EventType<T> eventType, EventHandler<? super T> eventHandler ) {

		if( eventHandler == null ) return null;
		
		String eventId = UUID.randomUUID().toString();
		
		eventList.add( "eventId",      eventId              );
		eventList.add( "eventType",    eventType );
		eventList.add( "eventHandler", eventHandler         );
		
		this.tableView.addEventHandler( eventType, eventHandler );
		
		return eventId;
		
	}
	
	/**
	 * 이벤트 추가시 돌려받았던 인덱스 값을 이용해 이벤트를 제거한다.
	 * 
	 * @param eventId 제거할 이벤트ID
	 */
	@SuppressWarnings("unchecked")
	public void removeEvent( String eventId ) {
		
		if( eventId == null ) return;
		
		NData event = null;

		for( int i = 0, iCnt = eventList.size(); i < iCnt; i++ ) {
			
			String id = eventList.getString( "eventId", i );
			
			if( id.equals(eventId) ) {
				event = eventList.get( i );
				eventList.set( i, null );
				break;
			}
			
		}
		
		if( event == null ) return;

		EventHandler eventHandler = (EventHandler) event.get( "eventHandler" );
		EventType    eventType    = (EventType)    event.get( "eventType"    );
		
		this.tableView.removeEventHandler( eventType, eventHandler );
		
	}
	
}

